文章目录
  1. 1. 前言
  2. 2. http基础知识
    1. 2.1. http的历史
    2. 2.2. http基础: 协议格式, https
    3. 2.3. http基础: http2.0
  3. 3. Android中流行的网络框架使用方法及其设计思想
    1. 3.1. Volley
      1. 3.1.1. 简介
      2. 3.1.2. 使用方法
      3. 3.1.3. 设计思想
      4. 3.1.4. 局限
    2. 3.2. OkHttp
      1. 3.2.1. 简介
      2. 3.2.2. 使用方法
      3. 3.2.3. 设计思想
      4. 3.2.4. 局限
    3. 3.3. Retrofit
      1. 3.3.1. 简介
      2. 3.3.2. 使用方法
      3. 3.3.3. 设计思想
      4. 3.3.4. 局限
  4. 4. 参考

前言

为方便初学者, 也为知识体系能够脉络清晰, 本文分为两个部分:

  • 第一部分讲述网络基础知识, 这是一切的基本功
  • 第二部分讲述几种常用的Android网络框架的使用方法和设计思想(不涉及源码解析)

由于Android官方提供的网络请求工具就是HttpURLConnection, 内容比较少就不再单独写了, 放在了第二部分. 各位可各取所需.

http基础知识

http的历史

我觉得了解一点技术的历史, 有助于理解该技术产生的思想内核, 以及时代局限, 但是网络这个话题实在太广泛, 因此不得不择其一二来讲述, 我选择了http及其相关技术, 不感兴趣的同学可以跳过.

http协议可以说是目前互联网使用最广泛的协议, 它在RFC中的定义历经二十年至今版本迭代才至2.0, 此前主要使用的是http1.0/1.1, 版本之间差异很大, 但不像别的技术, http由于影响面及其广, 所以在整个互联网中更新换代是非常缓慢的过程.

http是现在事实标准TCP/IP协议簇中的一员, 但它不为TCP/IP而生, 也不依赖于TCP/IP, 事实上只要保证传输稳定性, 它的下层用什么协议它并不关心(它自身位于协议栈的栈顶, 即应用层). 在上个世纪90年代, http诞生了, 它的主要设计人是Tim Berners Lee 博士, 这位牛人同时也是HTML, WWW和URL的主要贡献者, 是一个真正值得敬仰的前辈. http规定的是服务器和客户端之间, 或者说虚拟机和虚拟机之间, 数据传输的约定, 它从作用到内容其实并不神秘, 它的发展历程几乎可以等同于国际互联网的发展历程.

http全称是HyperText Transfer Protocol, 超文本传输协议. 名字好像挺玄乎, 其实也不神秘, 首先它把一段包含很多额外信息的文本称为超文本, 比如这段文本里面有URL链接, 代表另一段文本, 或者代表一个多媒体文件. 然后这个传输协议就是约定发送和接收双方如何来对待这段超文本, 发送方如何封包, 接收方如何解包, 这很像快递过程, 超文本被包裹在内, 其外放上一些防止内容被破坏的数据, 以及一些写着出发地和到达地的数据, 到了到达地, 再由收件人拆包, 获得超文本内容.

虽然说起来简单, 但这个协议实际上有长达60页的协议细节, 有兴趣的同学可以查看RFC-httpRFC-http1.1两个文档, 你如果熟知这两个文档的内容, 那下面有关http1.0/1.1的部分就完全不用看了.

http基础: 协议格式, https

以下内容主要参考维基百科Http-Wiki, RFC定义w3http1.1, 在线博客和一些计算机书籍

前面说了, http协议规定了怎么在客户端和服务端之间传输文件, 而规定的方式就是在传输数据包的时候, 在头尾加东西. 本节就来看看具体加了什么东西, 也就是http协议格式. 注意, 本节不讲tcp协议

教科书上都是由上到下, 先讲协议设计再讲具体数据, 但我想随着门槛的降低, 真正从事计算机相关工作且有坚实计算机网络基础的同学可能不多, 所以我从实践到理论地来讲http协议格式, 可能同学们更好理解些. 为方便说明, 我们可以任意生成一个http数据包, 比如, 我用Chrome自带的抓包工具chrome://net-internals/随便抓取了几个访问某https页面时生成的包(不带任何参数). 经过格式化解析之后的内容如下(#是我给的注释):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
t=695 [st=  0] +REQUEST_ALIVE  [dt=149]
t=695 [st= 0] DELEGATE_INFO [dt=6] # 请求代理信息
--> delegate_info = "NavigationResourceThrottle"
t=701 [st= 6] +URL_REQUEST_DELEGATE [dt=2]
t=701 [st= 6] DELEGATE_INFO [dt=2]
--> delegate_info = "扩展程序“AdBlock”"
t=703 [st= 8] -URL_REQUEST_DELEGATE
t=703 [st= 8] URL_REQUEST_START_JOB [dt=1] # URL请求类型
--> load_flags = 37121 (MAIN_FRAME_DEPRECATED | MAYBE_USER_GESTURE | VALIDATE_CACHE | VERIFY_EV_CERT) # 标志位
--> method = "GET" # 请求方法
--> priority = "HIGHEST" # 优先级
--> url = "https://www.baidu.com/" #请求的URL
t=704 [st= 9] +URL_REQUEST_START_JOB [dt=135]
--> load_flags = 37121 (MAIN_FRAME_DEPRECATED | MAYBE_USER_GESTURE | VALIDATE_CACHE | VERIFY_EV_CERT)
--> method = "GET"
--> priority = "HIGHEST"
--> url = "https://www.baidu.com/"
t=704 [st= 9] URL_REQUEST_DELEGATE [dt=0]
t=704 [st= 9] HTTP_CACHE_GET_BACKEND [dt=0]
t=704 [st= 9] HTTP_CACHE_OPEN_ENTRY [dt=0]
t=704 [st= 9] HTTP_CACHE_ADD_TO_ENTRY [dt=0]
t=704 [st= 9] HTTP_CACHE_READ_INFO [dt=0]
t=708 [st= 13] +HTTP_TRANSACTION_SEND_REQUEST [dt=0] # 请求事务类型
t=708 [st= 13] HTTP_TRANSACTION_SEND_REQUEST_HEADERS # 头部
--> GET / HTTP/1.1 # 方法, 协议, 版本
Host: www.baidu.com # 主机
Connection: keep-alive # 连接类型
Cache-Control: max-age=0 # 缓存
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36 # 用户代理
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 # 接受的数据类型
DNT: 1
Accept-Encoding: gzip, deflate, sdch, br # 接受的数据类型
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4 # 接受的语种
Cookie: [1203 bytes were stripped] # 附带的Cookie, 被解包工具跳过了
t=708 [st= 13] -HTTP_TRANSACTION_SEND_REQUEST
t=708 [st= 13] +HTTP_TRANSACTION_READ_HEADERS [dt=130] # 读取事务类型
t=708 [st= 13] HTTP_STREAM_PARSER_READ_HEADERS [dt=130] # 解析头部
t=838 [st=143] HTTP_TRANSACTION_READ_RESPONSE_HEADERS# 读取响应头部
--> HTTP/1.1 200 OK # 协议, 版本, 状态
Server: bfe/1.0.8.18
Date: Tue, 24 Jan 2017 02:26:34 GMT # 时间
Content-Type: text/html;charset=utf-8 # 内容类型, 字符集
Transfer-Encoding: chunked # 传输编码类型
Connection: keep-alive # 连接类型
Cache-Control: private # 缓存控制
Expires: Tue, 24 Jan 2017 02:26:34 GMT # 过期时间
Content-Encoding: gzip # 内容编码
X-UA-Compatible: IE=Edge,chrome=1 # UserAgent兼容
Strict-Transport-Security: max-age=172800
BDPAGETYPE: 2
BDQID: 0xa98302dc00005b89
BDUSERID: 27620551
Set-Cookie: [19 bytes were stripped] # 设置Cookie
Set-Cookie: [17 bytes were stripped]
Set-Cookie: [54 bytes were stripped]
Set-Cookie: [124 bytes were stripped]
t=838 [st=143] -HTTP_TRANSACTION_READ_HEADERS
t=838 [st=143] HTTP_CACHE_WRITE_INFO [dt=0]
t=838 [st=143] HTTP_CACHE_WRITE_DATA [dt=0]
t=838 [st=143] HTTP_CACHE_WRITE_INFO [dt=0]
t=838 [st=143] URL_REQUEST_DELEGATE [dt=1]
t=839 [st=144] URL_REQUEST_FILTERS_SET
--> filters = "FILTER_TYPE_GZIP"
t=839 [st=144] -URL_REQUEST_START_JOB
t=839 [st=144] HTTP_TRANSACTION_READ_BODY [dt=0] # 读取Body
t=839 [st=144] HTTP_CACHE_WRITE_DATA [dt=0] # 写缓存, 下面还有几次读取Body和写缓存
t=840 [st=145] URL_REQUEST_JOB_FILTERED_BYTES_READ
--> byte_count = 11343
t=840 [st=145] HTTP_TRANSACTION_READ_BODY [dt=0]
t=840 [st=145] HTTP_CACHE_WRITE_DATA [dt=0]
t=844 [st=149] -REQUEST_ALIVE

所以可以看到我们客户端的请求是一个比较简单的结构Header, 而主机返回的报文分成Header和Body两个部分. 先看我们发送的Header.

发送Header这个行为类型是URL_REQUEST_START_JOB HTTP_TRANSACTION_SEND_REQUEST, 我们发送的Header中method是GET, 表明我们想从服务器获取数据, http协议中允许的方法有以下几种:

  • GET: 用来请求访问已被URL识别的资源
  • POST: 用来传输实体的主体
  • PUT: 用来传输文件
  • HEAD: 和GET方法一样,只不过不返回实体报文
  • DELETE: 用来指定删除文件,与PUT方法相反
  • OPTION: 用来查询针对请求URL指定的资源支持的方法
  • TRACE: 让web服务器端将之前的请求通信还回给客户端的方法
  • CONNECT: 要求的与代理服务器通信时建立隧道,实现用隧道协议进行TCP通信

发送的包是否有消息体(Body)取决于方法, GET方法没有消息体, POST方法就有消息体. host是请求的主机, DNS域名解析, Socket连接建立等过程我们不去探究, 我们只要知道它代表了谁来处理这次请求. 接着看到了Connection, 这个字段表示连接选项, 可以告诉服务器怎么对待这次连接, 有closekeep-alive两种模式, 在0.9/1.0版本时, http连接默认是完成之后立即关闭, 需要设置为keep-alive模式才能保持连接, 在1.1以后, http连接默认就是keep-alive模式, 需要设置close模式才能断开连接. keep-alive的连接数过多的话就会引起服务器性能问题, 同一个页面如果交互次数多而不设置keep-alive的话, 会造成响应过慢的问题, 因为每次请求数据都要重新经历一次建立连接的过程. 注意, 即使设置了keep-alive也不能一定保证连接被保持. 用户代理(User-Agent)代表使用的软件(例如浏览器)是什么类型, 通过UA服务器可以区别对待响应内容, 比如Android系统/Chrome浏览器, 或者Mac系统/Safari浏览器等. Accept段表示可接受的媒介数据类型, 例如网页, 图片, 应用, 音频等. Accept-Encoding与之类似, 不过限制的对象是响应中的消息体编码, 例如允许gzip压缩文件等. Accept-Language表示页面语言, 没什么好说的, 注意它不同于页面编码.
至此Header内容就没有了, 如果是POST请求那么在此之后还有一个消息体代表要传输的内容.

接下来我们看服务器给回来的响应Response的格式(这里就不贴内容了, 依旧用chrome查看响应包), 其实可以看出它的格式与http请求的格式非常类似, 只不过多了一些响应状态的数据. 仍然先看Header, 第一行是响应协议, 响应状态代码, 以及响应状态. http的响应状态是一个三位数的数字编码, 大致意思是这样:

1
2
3
4
5
1xx:指示信息--表示请求已接收, 继续处理.
2xx:成功--表示请求已被成功接收、理解、接受. # 正常状态就是200
3xx:重定向--要完成请求必须进行更进一步的操作. # 例如重定向301 302
4xx:客户端错误--请求有语法错误或请求无法实现. # 403 404就在这里
5xx:服务器端错误--服务器未能实现合法的请求. # 500 Internal Server Error在这里

然后给了响应时间, 返回内容的类型(text/html)和编码(utf-8). 接着是Transfer-Encoding, 值为chunked. 这是一个比较重要的字段, 它代表这个响应本身以什么方式进行”传输编码”, 以保证能够正确传输. 例如, 这里的chunked代表要传输的消息体被分成一系列块(chunk)来传输, 每个块有自己的块长度, 这就是”流模式(streaming)”. 这意味着动态产生的内容就算很长, 并且服务端预先无法知道总长度, 也可以通过这种方式到达客户端, 只要它包含客户端用以判断是否传输完成的必要信息. 这个值也意味着会有后续消息传输, 也意味着连接应该被保持. 文档指出, http1.0及更旧版本的应用可能无法识别Transfer-Encoding这个属性. 后面的内容几乎是对应请求中的可接受内容相应值了, 同时这个响应还设置了cookie值. cookie这个东西挺有意思的, http是无连接的协议, 但有时候又真的需要一些已经生成的信息, 所以就需要把这些未来会用到的信息保存下来, cookie就起这么个作用. 再往后就是消息体了, 消息体里面会包含例如本段消息长度, 下段消息编号, 等这样的信息.

在http协议中, 没有对请求URL的长度和消息体的长度做任何限制, 但在实践中由于缓存, 性能, 传输效率, 安全等实际需求, 一般对URL长度和消息体大小都会做一定的限制, 这种限制既有客户端(浏览器)限制, 又有服务端限制. 另外, 从前面可以看出, cookie是放在请求的Header中一起发出的, 所以如果对请求Header大小有限制的话, 势必对cookie总大小也有限制. 实际上多数浏览器也确实限制了同一个域下cookie的数目.

这就是一次URL请求和响应的全部内容. 平时我们用到GET, POST, keep-alive等方法和属性比较多, 可能对其他几种方法和属性不太熟悉.

讲完了http协议格式, 现在来看https. https又叫 HTTP over TLS, 顾名思义是建立在一层TLS之上的http. TLS全称 Transport Layer Security, 传输层安全协议, 它的前身是SSL, Secure Sockets Layer. HTTPS协议下的URI格式与HTTP完全相同, 只不过它指示浏览器用一层SSL/TLS加密层来保护网络传输. HTTPS协议中, 在传统的HTTP之上又加了一层SSL/TLS, 所以说实现上是SSL/TLS在http之上, 逻辑上是http在SSL/TLS之上. SSL/TLS层创建了一个客户端和服务端之间的隧道, 所有传输都在这个隧道中完成, 这意味着http协议中传输的一切内容, 包括url, 都可以被加密(主机地址和端口号除外, 因为他们是TCP/IP协议的必需部分, 这意味着ip和端口可能被攻击者替换). SSL/TLS层负责服务端信任和传输安全保证.

http基础: http2.0

以下内容主要参考Http2-Wiki HTTP/2 Spechttp协议揭秘

理解了1.1协议之后, 2.0版本的协议就只需要重点看差异. 2.0版本协议是在2015年发布的, 正式名称是HTTP/2, 因为计划中不再有子版本, 下个版本的HTTP协议主版本号将是3. 在HTTP/2出现之前, 最流行的http1.1协议, 同一个TCP连接里面, 所有数据通信按次序进行, 服务器只有处理完一个回应, 才能进行下一个回应, 任意一个回应都可能成为瓶颈造成阻塞, 造成传输效率底下. 由于这是http1.1协议的缺陷, 因此我们只能设法避开这个问题, 一般有两个思路, 一是减少请求数, 二是多个持久连接, 很多网页优化技巧就是在此基础上开发出来的, 比如合并脚本和样式表, 将图片嵌入css代码, 域名分片等. 2009年, Google公开了它自主研发的SPDY协议, 其主要目的就是解决http1.1协议效率不高的问题, 并且在彼时已经在chrome浏览器上证明了其可行性. SPDY协议被当做HTTP/2协议的基础, 其主要特性在HTTP/2中都有体现.
相比http1.1, http/2有以下不同:

  • 纯二进制.
    在http1.1中头信息可以是文本(ASCII编码), 消息体可以是文本或二进制, 在http/2中, 头信息和消息体都是二进制, 被称为头信息帧(header frame)和数据帧. 这样做可以定义更多种类型的帧, 根据帧类型进行二进制数据解析, 显然比通过文本来获得数据类型, 再进行解析要快捷得多.
  • 多工.
    HTTP/2复用TCP连接, 在同一个连接里, 客户端和服务端都可以同时发送多个请求或响应, 而且不用按照顺序. 例如, 服务器同时收到A请求和B请求, 先响应A请求, 发现非常耗时, 于是把A请求已经处理好的部分发送回去, 再响应B请求并处理, 完成后发送A请求剩下的部分. 这样同时允许上行和下行数据的通信方式叫做双工通信, 服务器可以和多个客户端建立这种通信, 因此叫多工.
  • 数据流.
    HTTP/2中数据表不按顺序发送, 因此同一个连接里面连续的数据包可能属于不同的响应, 因此在数据包中有做标记表明它属于哪个响应. HTTP/2将一次响应对应的所有数据包, 称为一个数据流(stream), 每个数据流都有独一无二的编号, 每个数据包都必须标记数据流ID. 约定客户端发送的数据流ID一律为奇数, 服务端发送的则为偶数. 在http1.1中要在发送数据的中途取消数据发送, 唯一的方式就是关闭TCP连接, 但http/2中允许在任意时刻由任意一方发送取消信号(RST_STREAM帧)来取消数据流传输, 并且保持TCP连接活跃.
  • 头信息压缩.
    http1.1协议中, 由于连接本身不带状态, 所以每次请求都需要携带全部必须信息, 实际情况是同一次TCP连接时候携带的cookie, user agent这些信息往往是不变的, 导致不必要的重复发送, 浪费了带宽. 在http/2中, 引入头信息压缩机制, 一方面, 头信息被gzip或compress压缩后再发送, 另一方面, 客户端和服务端同时维护一张头信息表, 所有字段都会存入这个表, 生成一个索引号, 多次发送只发送索引号而不必发送重复的字段.
  • 服务器推送.
    http/2允许服务器主动向客户端发送资源, 即服务器推送(Server Push). 例如, 客户端请求一个页面, 该页面包含很多静态资源. 正常情况下客户端必须解析完返回的html之后, 再根据内容去请求静态资源, 但在http/2协议下, 服务端可以预判客户端在请求该网页后很可能会再次请求静态资源, 因此可以主动把静态资源跟网页一起发送给客户端.

Android中流行的网络框架使用方法及其设计思想

在Android源码中, 有两种访问网络的方式, 一种是使用HttpClient, 另一种是HttpURLConnection, Google曾选择前者, 后来切换到了后者, 原因见这篇文章, 大体是说HttpClient好是好bug也少但是太重, 相比之下Android上使用更轻量级的HttpURLConnection更合适. 在我看来技术本身没有好与不好之分, 只有合不合时宜, 至今我没有改变过这个看法. HttpClient是从Java继承来的, 是Apache基金会负责维护的网络客户端, 它好不好, 当然好, 大量的人在让它变得更好. 但是正因为它用的地方太广了, 需要照顾的人群和情景太多, 所以它在任何特定的情境下都很难作为最好的选择, 例如移动开发. 哪怕是它的兄弟项目AndroidHttpClient专门为Android而生, 也仍然携带了太多API内容, 以至于Android项目如果要使用它, 就会受到它的掣肘. 相反, HttpURLConnection提供的API就要少得多, 这让Android系统团队对它的扩展要容易得多.
我们打开SDK中的HttpURLConnection这个类, 可以发现它提供的API确实少, 它连同它的父类URLConnection提供的API都主要是面向上面我们介绍的http协议格式的, 可以很明确地知道我们如何调用它的API来构造一个URL请求.
一次网络请求很简单, 但是我们面临的问题往往是复杂的多次网络请求, 比如下载, 比如图片, 比如很多很多其他情况, 这就要求我们对所有需要的网络连接进行管理, 什么时候该开新长连接, 什么时候该关闭, 什么时候该新开线程运行, 什么时候需要缓存, 如何处理异常返回码等等, 而这里面大多数工作其实跟我们的业务没有什么关系. 做网络请求管理, 是一件令人心烦的事情, 有烦心事就会有人想要去解决, 于是Android上出现了诸多网络通信的库, 他们提供更高层级的封装和抽象, 面向业务逻辑而不是http报文格式来设计API. 他们提供缓存机制和连接管理机制, 他们让我们不需要关心网络连接的细节, 只需要关注业务逻辑.
下面我们就简单介绍其中较为流行的几种框架.

Volley

简介

名声很大, 大到什么程度呢? 一问你们用的什么网络框架, Volley都会出现在备选中. 它跟OkHttp应该算是两个主流网络请求框架了. 它是谷歌的亲儿子, 在2013年随着Google I/O推出, 作为一个网络请求模块, 独立于Android系统之外, 它像是把AsyncHttpClientUniversal-Image-Loader的优点都集于一身, 可以简单地进行http通信, 也可以轻松加载网络图片. Google出品不敢说全都是精品, 但至少是一流.
Volley在HttpURLConnection基础之上, 设计了简单易用的API, 同时在性能方面也有大幅优化, 设计目标是处理数据量不大但通信频繁的网络操作, 对于大数据量的网络操作则并不擅长.
频繁请求的场景是非常常见的, 例如新闻订阅, 定期更新的任何场景, 实时类应用, 像微博啊Twitter之类的都属于这一类.

使用方法

既然是独立模块, 自然需要单独下载安装使用.
本节主要参考Volley官方文档
我们可以通过源码下载Volley库:

git clone https://android.googlesource.com/platform/frameworks/volley
# 或者 https://github.com/google/volley

再用Android Studio或者SDK把它导出为jar包, 或者直接将这个库作为library module依赖.
也可以使用官方给出的gradle依赖配置:

compile 'com.android.volley:volley:1.0.0'

也可以使用别人放在Maven仓库的包(这个是个人行为, 不是官方放上去的, 并且现在已经停止维护了)

compile 'com.mcxiaoke.volley:library:1.0.6'

这位作者把Volley放到了Github上, 地址是 https://github.com/mcxiaoke/android-volley, 感谢他.
我们将会以一次Get请求和/或一次Post请求为例, 来说明Volley以及接下来几个框架的基本使用方法.

//简单的 GET 请求
mQueue = Volley.newRequestQueue(getApplicationContext());  
mQueue.add(new JsonObjectRequest(Method.GET, url, null,  
            new Listener() {  
                @Override  
                public void onResponse(JSONObject response) {  
                    Log.d(TAG, "response : " + response.toString());  
                }  
            }, null));  
mQueue.start();

这个例子是我在这篇文章里摘抄的. 从这些代码我们可以猜测一些东西(由于本文不涉及Volley源码解析, 所以我也还没读过源码, 只能猜测, 之后我会抽空把Volley源码和OkHttp源码都走一遍):

  1. Volley类恐怕是单例, 甚至不提供实例, 只提供静态方法. 原因是构造请求队列时调用静态方法, 使用了Context信息, 而且这个例子中使用的是Application Context, 在我有关Context的分析文章中说过, 这个Context实际就是整个Application.
  2. 基于接口. 显而易见, 就连响应类型都已经有预定义的接口了, 需要什么类型的请求直接找Volley的API就搞定, 而且显然即使官方没有也一定有人做了基于泛型的自定义接口. Volley在定义好这些接口之后, 基于这些接口就可以把缓存/并发/性能优化/图片优化/连接管理等功能给做了, 而完全不必关心请求具体是什么样子. 另外, 从队列add方法中新建的匿名类的回调来看, Volley的工作模式显然是异步的.
  3. 轮询. 这个是基于最后一句的猜测, 队列调用了start()方法想必是开始处理请求了, 这一点非常像Handler的使用中调用Looper.loop()方法开始处理消息队列, 所以这么猜测.

可以看到, 相较于HttpURLConnection, Volley的网络请求构造起来就简单多了, 任何细节我们都不必关心, 只需要给出最基本的参数(Method, URL)和一个回调.

设计思想

本节主要参考Volley设计思想和流程分析


volley原理图
啊这是个视频截图.

Volley中的请求数据流在RequestQueue中, 一切故事都围绕它展开. 原理图中我们可以看到Volley被设计为三种线程下的分工, 第一种当然是UI线程, 它负责将新产生的请求加入到请求队列, 以及对请求的响应做处理. 第二种是cache thread, 它负责请求及其响应的缓存. 第三种是网络线程, 它负责实际分发网络请求, 响应解析, 以及写缓存.

在故事开始, 我们通过Volley.newRequestQueue生成了一个请求队列, 并且如果打开源码稍微看一下就会知道Volley在此时就帮我们创建了缓存数据结构(DiskBasedCache)和网络连接线程池, 建立好请求队列之后, 我们同时拥有了缓存请求队列(在缓存线程)和网络请求队列(在网络线程).

接下来就是处理这些请求. 两种请求对应两种线程, CacheDispatcher处理缓存请求, 传入缓存请求队列和网络请求队列, 这两个队列的具体实现请看源码. 它先从缓存请求队列取出缓存请求, 在缓存中查找请求, 如果命中, 则直接从缓存取得响应进行分发, 如果没有命中或者缓存已过期, 则把这个请求放到网络请求队列中. NetworkDispatcher处理网络请求队列, 它先从网络请求队列中获取请求, 使用网络连接池进行实际的网络请求, 请求完成后如果返回结果可以缓存则存入缓存, 最后将响应结果分发.

到这里, Volley的魔术就只剩三个地方了: Cache管理, Request定义, 以及Request执行. 这是Volley高效的秘密所在. 先说Request执行. Volley定义了HttpStack.performRequest接口帮助定义Request执行过程, Request执行阶段, 默认的HttpStack实现会通过这个接口获取到Request实例和Headers, 构造http请求, 然后获取到响应的Response二进制流. 随后它调用Request的parseNetworkResponse将响应按照格式进行解析, 得到数据实体.

来看Request定义. Volley中预定义了几种常用的请求, 例如StringRequest, JsonRequest等, 所有这些请求中都有一个Listener<T>参数, T是请求所预期的返回数据类型. 这个参数就是我们在上面示例代码中添加的回调了, 在解析完Response之后, Volley便会调用Request.deliverResponse方法分发响应, 这跟Handler是不是很像? 在该方法中, 就是调用了Listener.onResponse来处理响应. 针对不同的请求, 可以对Request的定义有特定的优化.

再看缓存Cache. 前面说了, Volley默认使用DiskBasedCache作为缓存的数据结构. 这里我们不去看它的具体实现, 其实它不见得是最佳选择. 我们重点来看在整个请求生成到执行过程中Cache的作用. 假如没有缓存那么每次请求都会实实在在地被执行, 这在对某一静态资源做多次请求的时候显然不合适, 造成带宽和性能浪费. 缓存在这里的作用就是, 在每次新的请求发生的时候, 先检查是否可以由缓存提供响应, 如果可以就不会执行网络请求了, 这样网络负载就转化成了缓存读取负载, 在DiskBasedCache中就是文件读取负载(因为它把缓存写在了文件里). 在每次真正执行了请求之后, 如果请求预先设置了要进行缓存, 就会把这次请求的url连同它的响应, 包上一个CacheHeader(内含缓存大小等属性), 写入到SD卡中, 并在内存中保存一份<URL, CacheHeader>的映射, 用以判断是否存在缓存以及缓存是否有效.

关于缓存更详细的内容参见Volley的cache之硬盘缓存–DiskBasedCache

局限

前面说了Volley的原理, 基本上也就展现了它的优点: 面向Request接口, 灵活度高, 对Request有多个封装, 可以取消请求, 有缓存, 可以设置请求优先级, 可以有多个并发的网络连接, 自动异步执行等等. 它当然也有一些缺点或者说不足:

  1. ImageRequest, ImageLoader仍然不够高效. 这都是因为后来出了更高效的框架, 技术世界就是这样, 不服老不行.
  2. 默认不支持https

OkHttp

简介

大神JackWharton出品(他的其他”小作品”还包括: Retrofit, Butterknife, ActionBarSherlock, DiskLruCache… Github上年提交3k+, 精力之旺盛, 执行力之强, 我辈典范).

使用方法

使用方法参见我另一篇博客OkHttp源码探索

设计思想

设计思想其实在上面那篇博客的OkHttp架构图中可以看出. 总的来说是在http/https/SPDY协议基础上做了一层一对一的封装, 接管了缓存管理/请求管理/连接管理/响应解析等环节, 并分别进行了优化, 如多线程并发的请求和连接, 基于DiskLruCache的缓存管理, 非UI线程的响应等.

局限

OkHttp的优点是好用的同时保留了几乎和HttpURLConnection一样的接口, 学习成本较低, 但同时缺点也是它: API更接近协议, 对用户不够友好, 我们需要处理较多不属于业务的内容.

Retrofit

Retrofit其实是在OkHttp这样一个Http封装库的基础上再进行的一次封装(当然你也可以用别的方式完成请求), 它更多的是面向业务流程而非协议细节, 所以比上面两个妖艳贱货要高一个层次.

简介

RetrofitOkHttp都是属于Square公司的开源项目, 主力开发同样是JackWharton, 令人敬佩的同时也就理解了它为什么默认采用OkHttp作为底层请求库. 不得不说Okio+OkHttp+Retrofit这套组合不仅好用而且足够灵活和容易扩展, 是开源世界一道优美的风景.

使用方法

见我另一篇博客Retrofit源码探秘

设计思想

Retrofit将网络请求彻底视为路径/参数/响应函数的组合, 于是采用了annotation的方式来描述网络请求, 并用它来标注响应函数, 而把一切构造请求和获取响应的过程都藏到了身后, 开发者不需要知道背后的任何细节.
这其实跟Flask的服务端的思路有点相似.

局限

Retrofit的好处是隐藏了一切细节, 一切dirty work, 你只需要按照特定格式使用注解去定义一个请求, 其他的事情都可以交给他来做. 同时它保留了足够的灵活性和可扩展性, 实际的请求是由一个单独的请求工具完成的, 这个工具可以换成适合你业务的任意其他工具, 只需要实现一层中间接口.
以上都是它的优点, 那么它的缺点在哪里呢?
曾经的它无法取消正在进行的请求(而OkHttp是可以的), 也无法仅获取Json格式的字符串返回结果(它会自动使用Gson去解析成实例对象), 然而2.0+版本以后, 请求接口重新定义了之后已经可以像OkHttp那样轻松地取消请求, 同时它去掉了自动, 默认仅仅解析为字符串, 你需要自行添加对应Converter库依赖才能使用它为你实现的解析器, 或者你也可以自行实现一个解析器.
然而它依然有一些限制, 例如interface不能继承其他的Interface. 这还算可以接受, 毕竟每个业务完全应该有一套单独的业务接口, 应该通过接口的组合而不是继承来使用接口, 也许它现在最大的缺点就是1.x2.x完全不兼容了(就连这一点, 它也有提供向下兼容的API库来解决).

参考



update: 2017-02-27 我把OkHttp的源码大致走了一遍, 记录在这里
update: 2017-02-28 我把Retrofit的源码大致走了一遍, 记录在这里

文章目录
  1. 1. 前言
  2. 2. http基础知识
    1. 2.1. http的历史
    2. 2.2. http基础: 协议格式, https
    3. 2.3. http基础: http2.0
  3. 3. Android中流行的网络框架使用方法及其设计思想
    1. 3.1. Volley
      1. 3.1.1. 简介
      2. 3.1.2. 使用方法
      3. 3.1.3. 设计思想
      4. 3.1.4. 局限
    2. 3.2. OkHttp
      1. 3.2.1. 简介
      2. 3.2.2. 使用方法
      3. 3.2.3. 设计思想
      4. 3.2.4. 局限
    3. 3.3. Retrofit
      1. 3.3.1. 简介
      2. 3.3.2. 使用方法
      3. 3.3.3. 设计思想
      4. 3.3.4. 局限
  4. 4. 参考